// builtin
var fs = require('fs');
var path = require('path');

// vendor
var resv = require('resolve');

// given a path, create an array of node_module paths for it
// borrowed from substack/resolve
function nodeModulesPaths(start, cb) {
  var splitRe = process.platform === 'win32' ? /[\/\\]/ : /\/+/;
  var parts = start.split(splitRe);

  var dirs = [];
  for (var i = parts.length - 1; i >= 0; i--) {
    if (parts[i] === 'node_modules') continue;
    var dir = path.join.apply(
      path,
      parts.slice(0, i + 1).concat(['node_modules'])
    );
    if (!parts[0].match(/([A-Za-z]:)/)) {
      dir = '/' + dir;
    }
    dirs.push(dir);
  }
  return dirs;
}

function find_shims_in_package(pkgJson, cur_path, shims, browser) {
  try {
    var info = JSON.parse(pkgJson);
  } catch (err) {
    err.message = pkgJson + ' : ' + err.message;
    throw err;
  }

  var replacements = getReplacements(info, browser);

  // no replacements, skip shims
  if (!replacements) {
    return;
  }

  // if browser mapping is a string
  // then it just replaces the main entry point
  if (typeof replacements === 'string') {
    var key = path.resolve(cur_path, info.main || 'index.js');
    shims[key] = path.resolve(cur_path, replacements);
    return;
  }

  // http://nodejs.org/api/modules.html#modules_loading_from_node_modules_folders
  Object.keys(replacements).forEach(function(key) {
    var val;
    if (replacements[key] === false) {
      val = __dirname + '/empty.js';
    } else {
      val = replacements[key];
      // if target is a relative path, then resolve
      // otherwise we assume target is a module
      if (val[0] === '.') {
        val = path.resolve(cur_path, val);
      }
    }

    if (key[0] === '/' || key[0] === '.') {
      // if begins with / ../ or ./ then we must resolve to a full path
      key = path.resolve(cur_path, key);
    }
    shims[key] = val;
  });

  ['.js', '.json'].forEach(function(ext) {
    Object.keys(shims).forEach(function(key) {
      if (!shims[key + ext]) {
        shims[key + ext] = shims[key];
      }
    });
  });
}

// paths is mutated
// load shims from first package.json file found
function load_shims(paths, browser, readFile, cb) {
  // identify if our file should be replaced per the browser field
  // original filename|id -> replacement
  var shims = Object.create(null);

  (function next() {
    var cur_path = paths.shift();
    if (!cur_path) {
      return cb(null, shims);
    }

    var pkg_path = path.join(cur_path, 'package.json');

    readFile(pkg_path, 'utf8', function(err, data) {
      if (err) {
        // ignore paths we can't open
        // avoids an exists check
        if (err.code === 'ENOENT') {
          return next();
        }

        return cb(err);
      }
      try {
        find_shims_in_package(data, cur_path, shims, browser);
        return cb(null, shims);
      } catch (err) {
        return cb(err);
      }
    });
  })();
}

// paths is mutated
// synchronously load shims from first package.json file found
function load_shims_sync(paths, browser, readFileSync) {
  // identify if our file should be replaced per the browser field
  // original filename|id -> replacement
  var shims = Object.create(null);
  var cur_path;

  while ((cur_path = paths.shift())) {
    var pkg_path = path.join(cur_path, 'package.json');

    try {
      var data = readFileSync(pkg_path, 'utf8');
      find_shims_in_package(data, cur_path, shims, browser);
      return shims;
    } catch (err) {
      // ignore paths we can't open
      // avoids an exists check
      if (err.code === 'ENOENT') {
        continue;
      }

      throw err;
    }
  }
  return shims;
}

function build_resolve_opts(opts, base) {
  var packageFilter = opts.packageFilter;
  var browser = normalizeBrowserFieldName(opts.browser);

  opts.basedir = base;
  opts.packageFilter = function(info, pkgdir) {
    if (packageFilter) info = packageFilter(info, pkgdir);

    var replacements = getReplacements(info, browser);

    // no browser field, keep info unchanged
    if (!replacements) {
      return info;
    }

    info[browser] = replacements;

    // replace main
    if (typeof replacements === 'string') {
      info.main = replacements;
      return info;
    }

    var replace_main =
      replacements[info.main || './index.js'] ||
      replacements['./' + info.main || './index.js'];

    info.main = replace_main || info.main;
    return info;
  };

  var pathFilter = opts.pathFilter;
  opts.pathFilter = function(info, resvPath, relativePath) {
    if (relativePath[0] != '.') {
      relativePath = './' + relativePath;
    }
    var mappedPath;
    if (pathFilter) {
      mappedPath = pathFilter.apply(this, arguments);
    }
    if (mappedPath) {
      return mappedPath;
    }

    var replacements = info[browser];
    if (!replacements) {
      return;
    }

    mappedPath = replacements[relativePath];
    if (!mappedPath && path.extname(relativePath) === '') {
      mappedPath = replacements[relativePath + '.js'];
      if (!mappedPath) {
        mappedPath = replacements[relativePath + '.json'];
      }
    }
    return mappedPath;
  };

  return opts;
}

function resolve(id, opts, cb) {
  // opts.filename
  // opts.paths
  // opts.modules
  // opts.packageFilter

  opts = opts || {};
  opts.filename = opts.filename || '';

  var base = path.dirname(opts.filename);

  if (opts.basedir) {
    base = opts.basedir;
  }

  var paths = nodeModulesPaths(base);

  if (opts.paths) {
    paths.push.apply(paths, opts.paths);
  }

  paths = paths.map(function(p) {
    return path.dirname(p);
  });

  // we must always load shims because the browser field could shim out a module
  load_shims(paths, opts.browser, opts.readFile || fs.readFile, function(
    err,
    shims
  ) {
    if (err) {
      return cb(err);
    }

    var resid = path.resolve(opts.basedir || path.dirname(opts.filename), id);
    if (shims[id] || shims[resid]) {
      var xid = shims[id] ? id : resid;
      // if the shim was is an absolute path, it was fully resolved
      if (shims[xid][0] === '/') {
        return resv(shims[xid], build_resolve_opts(opts, base), function(
          err,
          full,
          pkg
        ) {
          cb(null, full, pkg);
        });
      }

      // module -> alt-module shims
      id = shims[xid];
    }

    var modules = opts.modules || Object.create(null);
    var shim_path = modules[id];
    if (shim_path) {
      return cb(null, shim_path);
    }

    // our browser field resolver
    // if browser field is an object tho?
    var full = resv(id, build_resolve_opts(opts, base), function(
      err,
      full,
      pkg
    ) {
      if (err) {
        return cb(err);
      }

      var resolved = shims ? shims[full] || full : full;
      cb(null, resolved, pkg);
    });
  });
}

resolve.sync = function(id, opts) {
  // opts.filename
  // opts.paths
  // opts.modules
  // opts.packageFilter

  opts = opts || {};
  opts.filename = opts.filename || '';

  var base = path.dirname(opts.filename);

  if (opts.basedir) {
    base = opts.basedir;
  }

  var paths = nodeModulesPaths(base);

  if (opts.paths) {
    paths.push.apply(paths, opts.paths);
  }

  paths = paths.map(function(p) {
    return path.dirname(p);
  });

  // we must always load shims because the browser field could shim out a module
  var shims = load_shims_sync(
    paths,
    opts.browser,
    opts.readFileSync || fs.readFileSync
  );

  if (shims[id]) {
    // if the shim was is an absolute path, it was fully resolved
    if (shims[id][0] === '/') {
      return shims[id];
    }

    // module -> alt-module shims
    id = shims[id];
  }

  var modules = opts.modules || Object.create(null);
  var shim_path = modules[id];
  if (shim_path) {
    return shim_path;
  }

  // our browser field resolver
  // if browser field is an object tho?
  var full = resv.sync(id, build_resolve_opts(opts, base));

  return shims ? shims[full] || full : full;
};

function normalizeBrowserFieldName(browser) {
  return browser || 'browser';
}

function getReplacements(info, browser) {
  browser = normalizeBrowserFieldName(browser);
  var replacements = info[browser] || info.browser;

  // support legacy browserify field for easier migration from legacy
  // many packages used this field historically
  if (typeof info.browserify === 'string' && !replacements) {
    replacements = info.browserify;
  }

  return replacements;
}

module.exports = resolve;
